package pl.dsp.dsp1.processing;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Signal extends ArrayList<SignalValue> {
	/**
	 * 
	 */
	private static final long serialVersionUID = 2424009450351462540L;

	private String name = "signal";

	private double samplingPeriod;

	public double getSamplingPeriod() {
		return samplingPeriod;
	}

	private double t1;

	public double getT1() {
		return t1;
	}

	public Signal(Signal original) {
		this.t1 = original.t1;
		this.samplingPeriod = original.samplingPeriod;
		this.addAll(original);
	}

	public Signal(double t1, double samplingPeriod) {
		this.t1 = t1;
		this.samplingPeriod = samplingPeriod;
	}
	
	public Signal(Generator g, double A, double t1,
			double d, double T, double samplingPeriod) {
		this.t1 = t1;
		this.samplingPeriod = samplingPeriod;
		
		if (g.isPeriodical())
			d = Math.floor(d / T) * T;

		int samples = (int) Math.floor(d / samplingPeriod) + 1;

		for (int i = 0; i < samples; i++) {
			ComplexValue multiplier = new ComplexValue(A, 0);
			this.add(g.at(samplingPeriod / T * i).multiply(multiplier));
		}
	}

	public Signal(GeneratorFactory.Source source, double A, double t1,
			double d, double T, double kw, double samplingPeriod) {
		this(GeneratorFactory.selectGenerator(source, kw), A, t1, d, T, samplingPeriod);
	}

	public List<Double> getXAxis() {
		ArrayList<Double> result = new ArrayList<Double>(this.size());
		for (int i = 0; i < this.size(); i++) {
			result.add(t1 + samplingPeriod * i);
		}
		return result;
	}

	public List<Double> getYAxis(int dimension) {
		ArrayList<Double> result = new ArrayList<Double>(this.size());
		for (int i = 0; i < this.size(); i++) {
			result.add(this.get(i).getValues().get(dimension));
		}
		return result;
	}

	public int getDimensions() {
		return this.get(0).getValues().size();
	}

	public boolean isCompatible(Signal another) {
		double periodsOfDifference = (this.t1 - another.t1)
				/ this.samplingPeriod;
		return this.samplingPeriod == another.samplingPeriod
				&& periodsOfDifference == Math.round(periodsOfDifference);
	}

	public Range getCommonRange(Signal another) {
		double thisT2 = this.t1 + this.size() * samplingPeriod;
		double anotherT2 = another.t1 + another.size() * samplingPeriod;

		double maxT1 = Math.max(this.t1, another.t1);
		double minT2 = Math.min(thisT2, anotherT2);

		return new Range(maxT1, minT2);
	}

	public Signal perform(SignalValueOperation operation, Signal another) {
		Range commonRange = getCommonRange(another);
		Signal result = new Signal(commonRange.getStart(), samplingPeriod);
		if (!isCompatible(another) || commonRange.length() < 0)
			return result;

		int thisStartingSample = (int) ((commonRange.getStart() - this.t1) / samplingPeriod);
		int anotherStartingSample = (int) ((commonRange.getStart() - another.t1) / samplingPeriod);

		for (int index = 0; index < Math
				.floor((commonRange.getEnd() - commonRange.getStart())
						/ samplingPeriod) - 1; index++) {
			result.add(operation.perform(this.get(thisStartingSample + index),
					another.get(anotherStartingSample + index)));
		}

		return result;
	}

	public Signal convolute(Signal another) {
		int M = this.size();
		int N = another.size();
		Signal result = new Signal(this.t1, this.samplingPeriod);

		for (int index = 0; index < M + N; index++) {
			SignalValue sum = new ComplexValue(0, 0);
			for (int k = 0; k < M; k++) {
				SignalValue anotherValue = (index - k) < 0 || (index - k) >= N ? new ComplexValue(
						0, 0) : another.get(index - k);
				sum = sum.add(this.get(k).multiply(anotherValue));
			}

			if (this.getDimensions() == 1)
				sum = new SimpleValue(sum);
			result.add(sum);
		}

		return result;
	}
	
	public Signal correlate(Signal another)
	{
		int M = this.size();
		int N = another.size();
		Signal result = new Signal(this.t1, this.samplingPeriod);

		for (int index = -N; index < M; index++) {
			SignalValue sum = new ComplexValue(0, 0);
			for (int k = 0; k < M; k++) {
				SignalValue anotherValue = (k - index) < 0 || (k - index) >= N ? new ComplexValue(
						0, 0) : another.get(k - index);
				sum = sum.add(this.get(k).conjugate().multiply(anotherValue));
			}

			if (this.getDimensions() == 1)
				sum = new SimpleValue(sum);
			result.add(sum);
		}

		return result;
	}
	
	public Signal correlate2(Signal another)
	{
		return this.perform(new ToConjugate(), this).convolute(another.inverse());
	}

	private Signal inverse() {
		Signal result = new Signal(this);
		Collections.reverse(result);
		return result;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Range getValueRange(int dimension) {
		Double[] values = (Double[]) getYAxis(dimension).toArray(new Double[1]);

		double minValue = Double.MAX_VALUE;
		double maxValue = Double.MIN_VALUE;

		// get minimum and maximum
		for (int i = 0; i < values.length; i++) {
			minValue = values[i] < minValue ? values[i] : minValue;
			maxValue = values[i] > maxValue ? values[i] : maxValue;
		}

		return new Range(minValue, maxValue);
	}

	public Signal quantize(int bitDepth) {
		int levels = 1 << bitDepth;
		Signal output = new Signal(getT1(), getSamplingPeriod());

		for (int dimension = 0; dimension < getDimensions(); dimension++) {
			Double[] values = (Double[]) getYAxis(dimension).toArray(
					new Double[1]);

			Range valueRange = getValueRange(dimension);

			double step = (valueRange.getEnd() - valueRange.getStart())
					/ (levels - 1);

			for (int index = 0; index < values.length; index++) {
				double quantized = valueRange.getStart()
						+ step
						* Math.floor((values[index] - valueRange.getStart())
								/ step);

				if (output.size() <= index) {
					output.add(new SimpleValue(quantized));
				} else {
					double oldValue = output.get(index).getValues().get(0);
					output.set(index, new ComplexValue(oldValue, quantized));
				}
			}
		}
		return output;
	}

	public Signal interpolate(Generator interpolator, double frequency) {
		double samplingPeriod = 1.0 / frequency;
		Signal result = new Signal(getT1(), samplingPeriod);

		double d = getSamplingPeriod() * (size() - 1);

		int samples = (int) Math.floor(d / samplingPeriod) + 1;
		for (int index = 0; index < samples; index++) {
			result.add(interpolator.at(getT1() + index * samplingPeriod));
		}

		return result;
	}
}
